package ace.tool.generate.code.impl.resolver.scheme.resolver.core;

import ace.cmp.util.CaseFormatUtils;
import ace.tool.generate.code.impl.resolver.scheme.model.bo.core.EntityClassInfo;
import ace.tool.generate.code.impl.resolver.scheme.model.bo.core.resources.LiquibaseTableFileInfo;
import ace.tool.generate.code.impl.resolver.scheme.model.bo.core.resources.model.dto.*;
import ace.tool.generate.code.impl.resolver.scheme.model.dto.ProjectInfo;
import ace.tool.generate.code.impl.service.model.input.GenerateIndexInput;
import ace.tool.generate.code.impl.service.model.input.GenerateInput;
import ace.tool.generate.code.impl.service.model.input.GenerateModelFieldInput;
import ace.tool.generate.code.impl.service.model.input.GenerateModelInput;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;

/**
 * @author caspar
 * @date 2023/11/17 10:34
 */
public class LiquibaseTableFileInfoResolver {
  private final GenerateInput input;
  private final GenerateModelInput modelInput;
  private final ProjectInfo coreProjectInfo;
  private final EntityClassInfo entityClassInfo;
  public static final Map<Class, String> DEFAULT_TYPE_MAP;
  private final Map<Class, String> typeMap;
  private final String defaultType;

  static {
    DEFAULT_TYPE_MAP = new HashMap<>();
    DEFAULT_TYPE_MAP.put(Integer.class, "INT");
    DEFAULT_TYPE_MAP.put(Long.class, "BIGINT");
    DEFAULT_TYPE_MAP.put(Boolean.class, "BOOLEAN");
    DEFAULT_TYPE_MAP.put(Date.class, "DATE");
    DEFAULT_TYPE_MAP.put(Time.class, "TIME");
    DEFAULT_TYPE_MAP.put(LocalDateTime.class, "DATETIME");
    DEFAULT_TYPE_MAP.put(UUID.class, "VARCHAR(36)");
    DEFAULT_TYPE_MAP.put(Short.class, "SMALLINT");
    DEFAULT_TYPE_MAP.put(BigInteger.class, "BIGINT");
    DEFAULT_TYPE_MAP.put(Timestamp.class, "TIMESTAMP");
    DEFAULT_TYPE_MAP.put(String.class, "VARCHAR(255)");
    DEFAULT_TYPE_MAP.put(BigDecimal.class, "DECIMAL(12,4)");
  }

  public LiquibaseTableFileInfoResolver(
      GenerateInput input,
      GenerateModelInput modelInput,
      ProjectInfo coreProjectInfo,
      EntityClassInfo entityClassInfo) {
    this.input = input;
    this.modelInput = modelInput;
    this.coreProjectInfo = coreProjectInfo;
    this.entityClassInfo = entityClassInfo;
    this.typeMap = DEFAULT_TYPE_MAP;
    this.defaultType = "VARCHAR(255)";
  }

  public LiquibaseTableFileInfo resolve() {
    return LiquibaseTableFileInfo.builder()
        .fileName(this.getFileName())
        .fileFullName(this.getFileFullName())
        .liquibaseTemplateModel(this.getModel())
        .build();
  }

  private LiquibaseTemplateModel getModel() {
    return LiquibaseTemplateModel.builder()
        .entityModelId(entityClassInfo.getEntityModelId())
        .name(entityClassInfo.getModel().getTableName())
        .remark(entityClassInfo.getModel().getRemark())
        .primaryKey(this.getPrimaryKey())
        .indexes(this.getIndexes())
        .fields(this.getFields())
        .build();
  }

  private List<LiquibaseTemplateModelField> getFields() {
    return entityClassInfo.getModel().getFields().stream()
        .map(this::getField)
        .collect(Collectors.toList());
  }

  private LiquibaseTemplateModelField getField(DaoEntityModelField entityModelField) {
    String name = CaseFormatUtils.convertUpperCamelToLowerUnderscore(entityModelField.getName());
    String dbType = this.getDbType(entityModelField);
    String defaultValue = this.getDefaultValue(entityModelField);
    String remark = this.getRemark(entityModelField);
    Boolean nullable = this.getNullable(entityModelField);
    return LiquibaseTemplateModelField.builder()
        .defaultValue(defaultValue)
        .idFlag(entityModelField.getIdFlag())
        .name(name)
        .nullable(nullable)
        .remark(remark)
        .type(dbType)
        .build();
  }

  private Boolean getNullable(DaoEntityModelField entityModelField) {
    Optional<GenerateModelFieldInput> modelFieldInputOptional =
        this.getGenerateModelFieldInput(entityModelField);
    if (modelFieldInputOptional.isEmpty()) {
      return true;
    }
    return modelFieldInputOptional.get().getNullable();
  }

  private String getRemark(DaoEntityModelField entityModelField) {
    Optional<GenerateModelFieldInput> modelFieldInputOptional =
        this.getGenerateModelFieldInput(entityModelField);
    if (modelFieldInputOptional.isEmpty()) {
      return entityModelField.getRemark();
    }
    return modelFieldInputOptional.get().getRemark();
  }

  private String getDefaultValue(DaoEntityModelField entityModelField) {
    Optional<GenerateModelFieldInput> modelFieldInputOptional =
        this.getGenerateModelFieldInput(entityModelField);
    if (modelFieldInputOptional.isEmpty()) {
      if (entityModelField.getDeleteFieldFlag()) {
        return "0";
      }
      if (entityModelField.getCreateTimeFieldFlag() || entityModelField.getUpdateTimeFieldFlag()) {
        return "(UNIX_TIMESTAMP(NOW(3))*1000)";
      }
      if (entityModelField.getRowVersionFieldFlag()) {
        return "1";
      }
      return null;
    }
    return modelFieldInputOptional.get().getDefaultValue();
  }

  private String getDbType(DaoEntityModelField entityModelField) {
    Optional<GenerateModelFieldInput> modelFieldInputOptional =
        this.getGenerateModelFieldInput(entityModelField);
    if (modelFieldInputOptional.isEmpty()) {
      return this.typeMap.getOrDefault(entityModelField.getTypeClass(), this.defaultType);
    }
    GenerateModelFieldInput modelFieldInput = modelFieldInputOptional.get();
    if (StringUtils.isNotEmpty(modelFieldInput.getDbType())) {
      return modelFieldInput.getDbType();
    }
    return this.typeMap.getOrDefault(modelFieldInput.getType(), defaultType);
  }

  private Optional<GenerateModelFieldInput> getGenerateModelFieldInput(
      DaoEntityModelField entityModelField) {
    return modelInput.getFields().stream()
        .filter(p -> p.getName().equals(entityModelField.getName()))
        .findFirst();
  }

  private List<LiquibaseTemplateModelIndex> getIndexes() {
    return Optional.ofNullable(modelInput).map(p -> p.getIndexes()).stream()
        .flatMap(List::stream)
        .map(this::getIndex)
        .collect(Collectors.toList());
  }

  private LiquibaseTemplateModelIndex getIndex(GenerateIndexInput indexInput) {
    String name = this.getIndexName(indexInput);
    return LiquibaseTemplateModelIndex.builder()
        .tableName(this.getTableName())
        .name(name)
        .columns(indexInput.getColumns())
        .uniqueFlag(indexInput.getUniqueFlag())
        .build();
  }

  private String getIndexName(GenerateIndexInput indexInput) {
    String name = indexInput.getName();
    if (StringUtils.isEmpty(name)) {
      String columnsString =
          indexInput.getColumns().stream()
              .map(p -> p.toLowerCase())
              .collect(Collectors.joining("_"));

      name =
          String.format(
              "%s_%s_%s",
              indexInput.getUniqueFlag() ? "uix" : "ix", this.getTableName(), columnsString);
    }
    return name;
  }

  private Optional<LiquibaseTemplateModelPrimaryKey> getPrimaryKey() {
    return entityClassInfo.getModel().getFields().stream()
        .filter(p -> p.getIdFlag())
        .map(
            p -> {
              String name = String.format("%s_id", this.getTableName());
              return LiquibaseTemplateModelPrimaryKey.builder()
                  .tableName(this.getTableName())
                  .name(name)
                  .columns(p.getName())
                  .build();
            })
        .findFirst();
  }

  private String getFileName() {
    return String.format("%s.xml", this.getTableName());
  }

  private String getTableName() {
    return entityClassInfo.getModel().getTableName();
  }

  private String getFileFullName() {
    Map<String, String> data = new HashMap<>();
    data.put("rootDir", coreProjectInfo.getResourcesRootDir());
    data.put("fileRootDir", input.getLiquibaseTableFileRootDir());
    data.put("fileName", this.getFileName());
    return StringSubstitutor.replace("${rootDir}/${fileRootDir}/${fileName}", data);
  }
}
